/*
 * Copyright (c) 2009 John Pritchard, WTKX Project Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package wtkx.ui;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphMetrics;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.util.StringTokenizer;

/**
 * This class lays- out and paints text.  It has two related
 * subclasses, {@link Caret} and {@link Selection}.
 * 
 * The superclass of {@link GV} is {@link java.awt.geom.Path2D$Float},
 * which is a write- once class employed by the subclasses of {@link
 * GV} for their own purposes.  The {@link Caret} creates a cursor
 * outline -- and the {@link Selection} creates a background fill area
 * -- into the path shape.
 * 
 * @see Caret
 * @see Selection
 * @see Position
 * @author jdp
 */
public class GV
    extends Path2D.Float
{
    /**
     * Marker for a group of text handling classes that are sensitive
     * to changes made here or among each other.
     * 
     * This class produces a particular distribution of line feed
     * characters within glyph vector lines.
     * 
     * The position class employs the trailing offset negative one for
     * home append mode.
     * 
     * @see Position
     * @see Caret
     * @see Selection
     */
    public interface Aware {
    }



    protected final GlyphVector[] glyphs;

    public final float leading, baseline, lineheight;

    public final float ix, iy, iw, ih;

    public final int length, textLength;


    /**
     * display: block;
     */
    public GV(String text, Font font, FontMetrics fm, boolean line, boolean flowed, 
              float ix, float iy, float iw, float ih)
    {
        super();
        this.ix = ix;
        this.iy = iy;
        this.iw = iw;
        this.ih = ih;
        this.leading = Math.max(1f,fm.getLeading());
        this.baseline = this.leading+fm.getAscent();
        this.lineheight = this.baseline + fm.getDescent();

        if (line)
            this.glyphs = toLine(text,font);

        else if (flowed){
            int w = (int)iw;
            if (0 < w && null != fm)
                this.glyphs = toFlowed(text,font,fm,w);
            else
                this.glyphs = toFixed(text,font);
        }
        else {
            this.glyphs = toFixed(text,font);
        }

        this.length = this.glyphs.length;
        this.textLength = ((null != text)?(text.length()):(0));
    }
    protected GV(GV gv){
        super();
        this.glyphs = gv.glyphs;
        this.leading = gv.leading;
        this.baseline = gv.baseline;
        this.lineheight = gv.lineheight;
        this.ix = gv.ix;
        this.iy = gv.iy;
        this.iw = gv.iw;
        this.ih = gv.ih;
        this.length = gv.length;
        this.textLength = gv.textLength;
    }


    public void paint(Graphics2D g, float x, float y){
        GlyphVector[] glyphs = this.glyphs;
        if (null != glyphs){
            y += this.baseline;

            float lh = this.lineheight;
            for (int gvline = 0, count = glyphs.length; gvline < count; gvline++){
                GlyphVector gv = glyphs[gvline];
                g.drawGlyphVector(gv,x,y);
                y += lh;
            }
        }
    }
    public int getOffsetFor(float x, float y){
        float lh = this.lineheight;
        if (0f < lh){
            float ry = (y - this.iy);
            if (0f < ry){
                GlyphVector[] glyphs = this.glyphs;
                if (null != glyphs){
                    int idxY = (int)(ry / lh);
                    if (-1 < idxY && idxY < glyphs.length){
                        GlyphVector gv = glyphs[idxY];

                        float rx = (x - this.ix);
                        int idxX = GV.OffsetIn(gv,rx);
                        /*
                         */
                        int ofs = 0;
                        for (int gvc = 0, gvcc = idxY; gvc < gvcc; gvc++){
                            gv = glyphs[gvc];
                            if (null != gv)
                                ofs += gv.getNumGlyphs();
                            else
                                throw new IllegalStateException();
                        }
                        return (ofs+idxX);
                    }
                }
            }
            else if (0f == ry)
                return 0;
        }
        return -1;
    }


    public final static Object RenderTextAliasing = Platform.getTextAntialiasingHint();
    public final static Object RenderTextFraction = RenderingHints.VALUE_FRACTIONALMETRICS_ON;
    public final static Object RenderAntialiasing = RenderingHints.VALUE_ANTIALIAS_ON;
    public final static FontRenderContext FRC = new FontRenderContext(new AffineTransform(),
                                                                      RenderTextAliasing,
                                                                      RenderTextFraction);

    public final static float[] PositionsLR(GlyphVector gv, int ofs, int cnt, float[] glp){
        if (0 > ofs)
            ofs = 0;

        glp = gv.getGlyphPositions(ofs,1,glp);
        float lhs = glp[0];

        ofs += (cnt-1);

        glp = gv.getGlyphPositions(ofs,1,glp);
        float rhs = glp[0];

        GlyphMetrics glm = gv.getGlyphMetrics(ofs);
        rhs += glm.getAdvanceX();

        glp[0] = lhs;
        glp[1] = rhs;
        return glp;
    }
    public final static float PositionL(GlyphVector gv, int ofs, float[] glp){
        if (0 > ofs)
            ofs = 0;

        glp = gv.getGlyphPositions(ofs,1,glp);

        return glp[0];
    }
    public final static float PositionR(GlyphVector gv, int ofs, float[] glp){
        if (0 > ofs)
            return PositionL(gv,0,glp);
        else {
            glp = gv.getGlyphPositions(ofs,1,glp);
            float rhs = glp[0];

            GlyphMetrics glm = gv.getGlyphMetrics(ofs);
            rhs += glm.getAdvanceX();

            return rhs;
        }
    }
    //     public final static int GetGlyphFromCharIndex(GlyphVector gv, int ofs){
    //         int glo = gv.getGlyphCharIndex(ofs);
    //         if (glo == ofs)
    //             return glo;
    //     }
    public final static int OffsetIn(GlyphVector gv, float rx){
        if (0f < rx){
            int cnt = gv.getNumGlyphs();
            if (0 < cnt){
                int glpl = (cnt<<1);
                float lhs, rhs, adv, jdx, jdc;
                float[] glp = gv.getGlyphPositions(0,cnt,(new float[glpl]));
                int glc = 0, glr, glo, jmp;
                float jdv = glp[glpl-2];
                jdv += gv.getGlyphMetrics(cnt-1).getAdvance();
                jdv /= ((float)cnt);

                for (; glc < glpl;){
                    lhs = glp[glc];
                    glo = (glc>>1);
                    glr = (glc+2);
                    if (glr != glpl){
                        rhs = glp[glr];
                        adv = (rhs-lhs);
                    }
                    else {
                        adv = gv.getGlyphMetrics(glo).getAdvanceX();
                        rhs = (lhs + adv);
                    }
                    if (lhs <= rx && rhs >= rx)
                        return gv.getGlyphCharIndex(glo);

                    else if (0f >= adv)
                        glc += 2;
                    else {
                        jdx = (rx - lhs);
                        jdc = (jdx / jdv)*2f;
                        if (0f > jdc)
                            glc -= 2;
                        else {
                            jmp = (int)jdc;
                            if (0 != (jmp & 1))
                                jmp ^= 1;

                            if (0 == jmp)
                                glc += 2;
                            else
                                glc += jmp;
                        }
                    }
                }
                if (0 < glc)
                    return (cnt);
            }
        }
        return 0;
    }

    private final static GlyphVector[] toLine(String line, Font font){
        if (null != line){
            GlyphVector gv = font.createGlyphVector(FRC,line);
            return (new GlyphVector[]{gv});
        }
        else {
            GlyphVector gv = font.createGlyphVector(FRC,"");
            return (new GlyphVector[]{gv});
        }
    }
    private final static GlyphVector[] toFlowed(String text, Font font, 
                                             FontMetrics fm, int w)
    {
        return toVector(FlowParagraphs(TextToParagraphs(text),fm,w),font);
    }
    private final static GlyphVector[] toFixed(String text, Font font){
        return toVector(TextToParagraphs(text),font);
    }
    //private final static int LayoutFlags = 6;
    private final static char[] CaryNil = new char[0];

    private final static GlyphVector[] toVector(String[] lines, Font font)
    {
        int count = (null != lines)?(lines.length):(0);
        if (0 < count){
            GlyphVector[] re = new GlyphVector[count];
            for (int cc = 0; cc < count; cc++){
                String line = lines[cc];
                if (null != line){
                    char[] cary = line.toCharArray();
                    GlyphVector gv = font.createGlyphVector(FRC,cary);
                    //GlyphVector gv = font.layoutGlyphVector(FRC,cary,0,cary.length,LayoutFlags);
                    re[cc] = gv;
                }
                else 
                    throw new IllegalStateException();
            }
            return re;
        }
        else {
            GlyphVector gv = font.createGlyphVector(FRC,CaryNil);
            return (new GlyphVector[]{gv});
        }
    }
    
    /**
     * <pre>
     * a\n      {"a","\n"}
     * a\nb     {"a","\nb"}
     * a\n\nb   {"a","\n","\nb"}
     * a\n\n\nb {"a","\n","\n","\nb"}
     * </pre>
     */
    private final static String[] TextToParagraphs(String text){
        if (null == text)
            return null;
        else {
            StringTokenizer strtok = new StringTokenizer(text,"\r\n",true);
            String tok = null, lines[] = null;
            boolean cr = false, lf = false;
            while (strtok.hasMoreTokens()){

                tok = strtok.nextToken();
                switch (tok.charAt(0)){
                case '\r':
                    cr = true;
                    break;
                case '\n':
                    cr = false;
                    lf = true;
                    lines = Add(tok,lines);
                    break;
                default:
                    if (lf){
                        cr = false;
                        lf = false;
                        if (null != lines){
                            int lidx = lines.length-1;
                            lines[lidx] = (lines[lidx] + tok);
                        }
                        else
                            lines = Add(tok,lines);
                    }
                    else if (cr){
                        cr = false;
                        lines = Add("\n",lines);
                        lines = Add(tok,lines);
                    }
                    else {
                        lines = Add(tok,lines);
                    }
                    break;
                }
            }
            return lines;
        }
    }
    private final static String[] FlowParagraphs(String[] paragraphs, FontMetrics fm, int width){
        if (null == paragraphs)
            return null;
        else {
            String[] lines = null;
            int count = paragraphs.length;
            for (int cc = 0; cc < count; cc++){

                String line = paragraphs[cc];
                int linew = fm.stringWidth(line);
                if (linew > width){
                    StringBuilder store = new StringBuilder();
                    String save, check;
                    int checkWidth;
                    StringTokenizer strtok = new StringTokenizer(line," \t",true);
                    while (strtok.hasMoreTokens()){
                        save = store.toString();
                        String token = strtok.nextToken();
                        switch (token.charAt(0)){
                        case ' ':
                            store.append(' ');
                            check = store.toString();
                            checkWidth = fm.stringWidth(check);
                            if (checkWidth >= width){
                                lines = Add(save,lines);
                                store.setLength(0);
                            }
                            break;
                        case '\t':
                            store.append("    ");
                            check = store.toString();
                            checkWidth = fm.stringWidth(check);
                            if (checkWidth >= width){
                                lines = Add(save,lines);
                                store.setLength(0);
                            }
                            break;
                        default:
                            store.append(token);
                            check = store.toString();
                            checkWidth = fm.stringWidth(check);
                            if (checkWidth >= width){
                                lines = Add(save,lines);
                                store.setLength(0);
                                store.append(token);
                            }
                            break;
                        }
                    }
                    if (0 < store.length()){
                        save = store.toString();
                        lines = Add(save,lines);
                    }
                }
                else {
                    lines = Add(line,lines);
                }
            }
            return lines;
        }
    }

    private final static GlyphVector[] Add(GlyphVector c, GlyphVector[] list){
        if (null == list)
            return new GlyphVector[]{c};
        else {
            int len = list.length; 
            GlyphVector[] copier = new GlyphVector[len+1];
            System.arraycopy(list,0,copier,0,len);
            copier[len] = c;
            return copier;
        }
    }
    private final static String[] Add(String c, String[] list){
        if (null == list)
            return new String[]{c};
        else {
            int len = list.length; 
            String[] copier = new String[len+1];
            System.arraycopy(list,0,copier,0,len);
            copier[len] = c;
            return copier;
        }
    }
}
